5.02. Исключения
Исключения
Исключения — это механизм, позволяющий обрабатывать ошибки и аномальные ситуации во время выполнения программы. В отличие от возврата кодов ошибок, исключения обеспечивают чистое разделение нормального потока выполнения и обработки сбоев, предотвращая распространение ошибок по стеку вызовов без явного управления.
В Python используется модель исключений с раскруткой стека (stack unwinding): когда исключение возникает, интерпретатор останавливает текущее выполнение, "раскручивает" стек в поисках блока except, и передаёт управление соответствующему обработчику. Если подходящий обработчик не найден — программа аварийно завершается с трассировкой стека (traceback).
Все исключения в Python — классы, наследующие от базового класса BaseException. Однако пользовательские и стандартные исключения, как правило, наследуются от Exception — подкласса BaseException.
BaseException
├── SystemExit # sys.exit()
├── KeyboardInterrupt # Ctrl+C
├── GeneratorExit # закрытие генератора
└── Exception # все остальные
├── StopIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── ValueError
├── TypeError
├── NameError
├── AttributeError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── ...
├── ImportError
│ └── ModuleNotFoundError
└── ... (и многие другие)
SystemExit, KeyboardInterrupt, GeneratorExit — не должны перехватываться в общих except блоках, так как они управляют жизненным циклом программы.
Перехват всех исключений через except Exception: безопаснее, чем except BaseException:, поскольку позволяет программе корректно реагировать на прерывания и завершения.
Синтаксис обработки исключений.
- Блок try-except
try:
risky_operation()
except SpecificError as e:
handle_error(e)
Можно перехватывать несколько типов:
except (ValueError, TypeError) as e:
print(f"Invalid input: {e}")
Или использовать отдельные блоки:
except ValueError:
print("Not a number")
except TypeError:
print("Wrong type")
Порядок важен: более специфичные исключения должны идти первыми.
- Блок else.
Выполняется, только если в try не было исключений.
try:
data = open_file()
except FileNotFoundError:
print("File not found")
else:
process(data) # только если файл успешно открыт
- Блок finally. Выполняется всегда, независимо от наличия исключения. Используется для освобождения ресурсов.
f = None
try:
f = open("file.txt")
process(f)
except IOError:
print("IO error")
finally:
if f:
f.close() # гарантированное закрытие
Аналогичный, но более удобный способ — использование менеджеров контекста (with).
- Полная форма.
try:
operation()
except SpecificError as e:
log(e)
raise # повторное возбуждение
except AnotherError:
fallback()
else:
commit()
finally:
cleanup()
Генерация исключений: raise.
Явное возбуждение исключения:
if x < 0:
raise ValueError("x must be non-negative")
Можно передать экземпляр:
raise ValueError("Invalid value")
# или
exc = ValueError("Invalid")
raise exc
Повторное возбождение (raise без аргументов).
Используется внутри except для передачи исключения дальше, сохраняя оригинальную трассировку:
except ValueError as e:
log_error(e)
raise # проброс с сохранением traceback
Возбуждение с другим контекстом: raise ... from.
Позволяет указать причину (chained exception):
try:
int("abc")
except ValueError as e:
raise TypeError("Parsing failed") from e
Результат:
Traceback (most recent call last):
File "...", line 3, in <module>
int("abc")
ValueError: invalid literal for int()
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "...", line 5, in <module>
raise TypeError("Parsing failed") from e
TypeError: Parsing failed
from None подавляет цепочку:
raise NewError("msg") from None
Для моделирования предметной области рекомендуется определять собственные иерархии исключений:
class AppError(Exception):
"""Базовое исключение приложения."""
pass
class ValidationError(AppError):
"""Ошибка валидации входных данных."""
def __init__(self, field, message):
super().__init__(f"Validation error in '{field}': {message}")
self.field = field
class AuthError(AppError):
"""Ошибка аутентификации."""
pass
Использование:
if not token_valid(token):
raise AuthError("Token expired or invalid")
try:
validate_user(data)
except ValidationError as e:
print(f"Field {e.field} is invalid: {e}")
Контекстные менеджеры и __exit__.
Обработка исключений тесно связана с протоколом контекстных менеджеров. Объект становится таковым, если реализует методы __enter__ и __exit__.
Метод __exit__(self, exc_type, exc_val, exc_tb) получает информацию об исключении:
- exc_type — класс исключения (None, если нет).
- exc_val — экземпляр исключения.
- exc_tb — объект трассировки (traceback).
Если __exit__ возвращает True, исключение подавляется. В противном случае — продолжает распространяться.
class suppress:
def __init__(self, *exceptions):
self.exceptions = exceptions
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, tb):
return exc_type is not None and issubclass(exc_type, self.exceptions)
# Использование:
with suppress(FileNotFoundError):
os.remove("temp.tmp") # ошибка игнорируется
В асинхронном коде (async/await) исключения обрабатываются аналогично, но с учётом событийного цикла:
async def fetch():
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status != 200:
raise HttpError(resp.status)
return await resp.text()
except aiohttp.ClientError as e:
logger.error(f"Network error: {e}")
raise
Особенность: исключения в корутинах не возникают немедленно — они "запускаются" при await. Поэтому важно правильно управлять временем жизни задач.